package com.pingan.pastry;

import com.pingan.pastry.common.annotation.Param;
import com.pingan.pastry.common.exception.InvokeException;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;

public class PastryManager extends ClassLoader implements Opcodes {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    public byte[] dumpRequest(Method method) {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        String requestName = obtainClassName(method, "Request");
        String className = requestName.replaceAll("\\.", "/");
        Parameter[] parameters = method.getParameters();
        String requestInterface = "com.pingan.pastry.Request".replaceAll("\\.", "/");

        cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", new String[]{requestInterface});
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        //生成构造方法
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
        for (Parameter parameter : parameters) {
            Param annotation = parameter.getAnnotation(Param.class);
            String paramName = annotation.value();
            Type type = parameter.getParameterizedType();
            String paramType = convertParamType(type);

            //生成field
            cw.visitField(ACC_PRIVATE, paramName, paramType, null, null);

            //生成get方法
            System.out.println("funName: " + obtainFunctionName(paramName, "get") + " ==paramType: " + paramType);
            mv = cw.visitMethod(ACC_PUBLIC, obtainFunctionName(paramName, "get"), "()" + paramType, null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, className, paramName, paramType);
            mv.visitInsn(ARETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
            //生成set方法
            mv = cw.visitMethod(ACC_PUBLIC, obtainFunctionName(paramName, "set"), "(" + paramType + ")V", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitFieldInsn(PUTFIELD, className, paramName, paramType);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }
        return cw.toByteArray();
    }

    public byte[] dumpInvoker(Method method) {
        Class<?> serviceType = method.getDeclaringClass();
        String invokerName = obtainClassName(method, "Invoker");
        String className = invokerName.replaceAll("\\.", "/");
        String invokerDesc = "com.pingan.pastry.proxy.Invoker".replaceAll("\\.", "/");
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", new String[]{invokerDesc});
        //cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, invokerName, null, "java/lang/Object", null);
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        //生成构造器方法
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();

        //生成 field
        cw.visitField(ACC_PRIVATE, "target", "Ljava/lang/Object;", null, null);
        //生成get方法
        mv = cw.visitMethod(ACC_PUBLIC, "getRequestType", "()Ljava/lang/Class;", "()Ljava/lang/Class;<*>", null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        String requestClassName = obtainRequestDesc(method);
        System.out.println("requestClassName： " + requestClassName);
        mv.visitLdcInsn(org.objectweb.asm.Type.getType(requestClassName));
        mv.visitInsn(ARETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        //getTarget
        mv = cw.visitMethod(ACC_PUBLIC, "getTarget", "()Ljava/lang/Object;", null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "target", "Ljava/lang/Object;");
        mv.visitInsn(ARETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
        //setTarget
        mv = cw.visitMethod(ACC_PUBLIC, "setTarget", "(Ljava/lang/Object;)V", null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitFieldInsn(PUTFIELD, className, "target", "Ljava/lang/Object;");
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        //生成invoke方法
        Parameter[] parameters = method.getParameters();
        Class<?> returnType = method.getReturnType();
        //有参 requestClassName
        if (parameters.length > 0) {
            mv = cw.visitMethod(ACC_PUBLIC, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;", null, new String[]{"java/lang/Exception"});
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 1);
            String requestClass = obtainClassName(method, "Request").replaceAll("\\.", "/");
            mv.visitTypeInsn(CHECKCAST, requestClass);
            mv.visitVarInsn(ASTORE, 2);
            /*mv.visitTypeInsn(NEW, "com/pingan/pastry/Response");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "com/pingan/pastry/Response", "<init>", "()V", false);
            mv.visitVarInsn(ASTORE, 3);*/
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, className, "target", "Ljava/lang/Object;");
            String targetClassName = method.getDeclaringClass().getName();
            targetClassName = targetClassName.replaceAll("\\.", "/");
            mv.visitTypeInsn(CHECKCAST, targetClassName);
            mv.visitVarInsn(ASTORE, 3);
            String targetRequestName = obtainClassName(method, "Request").replaceAll("\\.", "/");
            /*mv.visitVarInsn(ALOAD, 3);*/
            mv.visitVarInsn(ALOAD, 3);
            for (int i = 0; i < parameters.length; i++) {
                mv.visitVarInsn(ALOAD, 2);
                Parameter parameter = parameters[i];
                Type parameterType = parameter.getParameterizedType();
                String paramType = convertParamType(parameterType);
                Param annotation = parameter.getAnnotation(Param.class);
                if (annotation == null) {
                    LOGGER.error("invoker param annotation is null, please check " + invokerName + " @Param");
                    throw new InvokeException("invoker create failed," + invokerName + "@Param is null");
                }
                String paramName = annotation.value();
                String functionName = obtainFunctionName(paramName, "get");
                System.out.println("targetRequestName: " + targetRequestName + "==getFunctionName:" + functionName + "==paramType: " + paramType);
                mv.visitMethodInsn(INVOKEVIRTUAL, targetRequestName, functionName, "()" + paramType, false);
            }
            String methodName = method.getName();
            if (serviceType.isInterface()) {
                mv.visitMethodInsn(INVOKEINTERFACE, targetClassName, methodName, org.objectweb.asm.Type.getMethodDescriptor(method), true);
            } else {
                mv.visitMethodInsn(INVOKEVIRTUAL, targetClassName, methodName, org.objectweb.asm.Type.getMethodDescriptor(method), false);
            }
            mv.visitVarInsn(ASTORE, 4);
            //mv.visitMethodInsn(INVOKEVIRTUAL, "com/pingan/pastry/Response", "setData", "(" + org.objectweb.asm.Type.getDescriptor(returnType) + ")V", false);
            mv.visitVarInsn(ALOAD, 4);
            mv.visitInsn(ARETURN);
            mv.visitMaxs(3, 4);
            mv.visitEnd();
            cw.visitEnd();
        } else {
            //无参

        }
        return cw.toByteArray();
    }


    public String obtainClassName(Method method, String type) {
        String name = (method.getDeclaringClass().getName() + "." + method.getName() + "." + type).trim();
        return name;
    }


    private String obtainFunctionName(String paramName, String functionName) {
        paramName = paramName.substring(0, 1).toUpperCase() + paramName.substring(1, paramName.length());
        String funName = functionName + paramName;
        return funName;
    }

    private String convertParamType(Type type) {
        String typeName = type.getTypeName();
        switch (typeName) {
            case "int":
                return "I";
            case "double":
                return "D";
            case "boolean":
                return "Z";
            case "long":
                return "J";
            case "float":
                return "F";
            case "char":
                return "C";
            case "java.lang.Object":
                return "java.lang.Object";
        }

        if (typeName.startsWith("java")) {
            String name = typeName.replaceAll("\\.", "/");
            typeName = "L" + name + ";";
        }
        return typeName;
    }

    @Override
    protected Class<?> findClass(String name) {
        Class<?> classType = null;
        try {
            super.findClass(name);
        } catch (ClassNotFoundException e) {
            String serviceId = name.substring(0, name.lastIndexOf("."));
            Method method = obtainMethodByServiceId(serviceId);
            String suffix = name.substring(name.lastIndexOf(".") + 1, name.length());
            if ("Request".equals(suffix)) {
                classType = processRequestType(method);
            } else if ("Invoker".equals(suffix)) {
                classType = processInvoker(method);
            }
        }
        return classType;
    }


    public Method obtainMethodByServiceId(String serviceId) {
        String className = serviceId.substring(0, serviceId.lastIndexOf("."));
        String methodName = serviceId.substring(serviceId.lastIndexOf(".") + 1, serviceId.length());
        Method targetMethod = null;
        try {
            Class<?> classType = loadClass(className);
            Method[] methods = classType.getMethods();
            for (Method method : methods) {
                if (methodName.equals(method.getName())) {
                    targetMethod = method;
                }
            }
        } catch (ClassNotFoundException e) {
            LOGGER.error("load class error, serviceId is " + serviceId);
            e.printStackTrace();
        }
        return targetMethod;
    }

    private String obtainRequestDesc(Method method) {
        String className = obtainClassName(method, "Request");
        className = className.replaceAll("\\.", "/");
        return "L" + className + ";";
    }


    private Class<?> processRequestType(Method method) {
        //LOGGER.info(" obtain " + name + " request start");
        //生成 Request
        byte[] bytes = dumpRequest(method);
        createClass(bytes, "Request1.class");
        String requestName = obtainClassName(method, "Request");
        Class<?> requestClass = defineClass(requestName, bytes, 0, bytes.length);
        System.out.println("requestClass: " + requestClass.getName());
        return requestClass;
        //LOGGER.info(" obtain " + name + " request end");
    }


    private Class<?> processInvoker(Method method) {
        byte[] invokers = dumpInvoker(method);
        createClass(invokers, "Invoker1.class");
        String invokerName = obtainClassName(method, "Invoker");
        Class<?> invokerClass = defineClass(invokerName, invokers, 0, invokers.length);
        System.out.println("invoker: " + invokerClass.getName());
        return invokerClass;
    }

    public void createClass(byte[] bytes, String type) {
        String filepath = "F:\\studyProject\\pastry\\pastry-app\\target\\classes\\" + type;
        FileOutputStream fos = null;
        try {
            File file = new File(filepath);
            fos = new FileOutputStream(filepath);
            fos.write(bytes);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public Class<?> createInvoker(String name) throws ClassNotFoundException {
        return loadClass(name);
    }

    public static void main(String[] args) throws NoSuchMethodException {
        /*Method method = Response.class.getMethod("getCode", null);
        System.out.println(method.getDeclaringClass().getName());*/
    }

}
